package stephencarmody.fonttexture;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;

public final class Texture extends JLabel {
    
	private final class GlyphMetrics {
		
	    public float x, y, ascent, boundx, boundy, boundwidth, boundheight;
	    
	    /** Sets metrics */
	    public void setMetrics(java.awt.font.GlyphMetrics gm) {
	    	Rectangle2D bounds = gm.getBounds2D();

	    	boundwidth = (float)(bounds.getWidth() + 2);
            boundheight = (float)(bounds.getHeight() + 2);

            // Check for start of new row
            if ((nextx + boundwidth) > image.getWidth()) {
                nexty += maxHeight;
                nextx = maxHeight = 0;
            }

	        ascent = (float)-bounds.getY();
            x = nextx - (float)bounds.getX() + 1;
	    	y = nexty + ascent + 1;
	    	boundx = nextx;
	    	boundy = nexty;
	        
            if (boundheight > maxHeight) {
                maxHeight = boundheight;
            }
            
	        nextx += boundwidth;
	    }
	    
	}
	
    private static final long serialVersionUID = 0;
 
    // Array of characters to draw to texture
    private static char[] chars = "{|}$()[\\]j/@QABCDEFGHIJKLMNOPRSTUVWXYZ#%&0123456789bdfghiklpqy!?;tacemnorsuvwxz+:<>~',-.*^_`=\"".toCharArray(); 

    // Image of texture, graphics and fontrender contexts
    private BufferedImage image;
    private Graphics2D g;
    private FontRenderContext frc;
    
    // Array of metrics
    private GlyphMetrics[] metrics;
    
    // Calculated size of font
    private int size;
    
    // The Font used
    private Font font;
    private String name;
    private int style;

	// Layout variables
	private static float nextx, nexty, maxHeight;
    
    /** Creates a new instance of Texture */
    public Texture(int size) {
        // Create and initialise array of GlyphMetrics
        metrics = new GlyphMetrics[chars.length];
        for ( int i = 0; i < chars.length; i++ ) {
        	metrics[i] = new GlyphMetrics();
        }
        setSize(size);
    }
    
    /**
     * Sets the size of the texture.
     * 
     * size is rounded to the nearest value that meets the rule:
     * 
     * 		log(size) / log(2) = whole integer
     * 
     * i.e. 2, 4, 8, 16, 32, 64, 128, 256, 512, etc... 
     */
    public int setSize(int size) {
    	double n = Math.log(size) / Math.log(2);
		size = (int) Math.pow(2, Math.round(n));
		if ( image != null && image.getWidth() == size ) return size;
		image = new BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY);
		g = (Graphics2D) image.getGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        frc = g.getFontRenderContext();
        layoutGlyphs();
        return size;
    }
    
    /** Set the font. */
    public void setFont(String name, int style) {
    	if ( this.name == name && this.style == style ) return;
        this.name = name;
        this.style = style;
        layoutGlyphs();
    }
    
    /**
     * Saves font texture to specified file.
     *  
     * @throws IOException
     */
    public void save(File file) throws IOException {
        // Write the texture to the specified file
        ImageIO.write(image, ImageFilter.getExtension(file), file);
        
        // Open a data output stream on the file to append metrics
        FileOutputStream fstream = new FileOutputStream(file, true);
        DataOutputStream out = new DataOutputStream(fstream);

        // Start with a marker so we can determine which PNG's are font textures
        out.writeBytes("FTX");
        
        // Write character|x|y|width|height|ascent for each glyph
        for ( int i = 0; i < chars.length; i++ ) {
            GlyphMetrics g = metrics[i];
            out.writeByte(chars[i]);
            out.writeInt(Math.round(g.boundx));
            out.writeInt(Math.round(g.boundy));
            out.writeByte(Math.round(g.boundwidth));
            out.writeByte(Math.round(g.boundheight));
            out.writeByte(Math.round(g.ascent));
        }
        // Close the stream
        out.close();
    }
    
    /** Layouts out glyphs on the texture. */
    private void layoutGlyphs() {
    	if ( name == null ) return;
    	
    	GlyphVector glyphVector;
    	java.awt.font.GlyphMetrics gm = null;
    	int lowest = 1;
    	int highest = 200;
    	float overrun;
    	int lastsize = 0;
    	
        size = 100;
       
        // Binary search for optimal font size
        do {
        	font = new Font(name, style, size);
        	glyphVector = font.createGlyphVector(frc, chars);
            nextx = maxHeight = 0;
            nexty = 1;
        	for (int i = 0; i < chars.length; i++) {
        		gm = glyphVector.getGlyphMetrics(i);
            	metrics[i].setMetrics(gm);
        	}
        	overrun = (metrics[chars.length-1].boundy + maxHeight) - image.getHeight();
        	if ( overrun > 0 ) {
        		lastsize = highest = size;
        	} else {
        		lastsize = lowest = size;
        	}
    		size = (lowest + highest) / 2;
        } while ( overrun > 0 || lastsize != size );

        // Clear image, set font and colour
        g.clearRect(0, 0, image.getWidth(), image.getHeight());
        g.setFont(font);
        g.setColor(Color.WHITE);

        // Draw each glyph to the texture
        for (int i = 0; i < chars.length; i++) {
        	g.drawChars(chars, i, 1, (int)Math.ceil(metrics[i].x),
                    (int)Math.ceil(metrics[i].y));
        }
        
        setIcon(new ImageIcon(image));
    }
    
}
